/***************************************************************************
 *   Copyright (C) 2005 - 2006 by                                          *
 *      Christian Muehlhaeuser, Last.fm Ltd <chris@last.fm>                *
 *      Erik Jaelevik, Last.fm Ltd <erik@last.fm>                          *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Steet, Fifth Floor, Boston, MA  02111-1307, USA.          *
 ***************************************************************************/

#include <QDebug>
#include <QDomDocument>
#include <QFile>
#include <QTcpSocket>
#include <QTimer>
#include <QtGui>
#include <QtXml>

#include "containerutils.h"
#include "webservice.h"
#include "Loqqer.h"
#include "utils.h"

#ifdef WIN32
    #include <windows.h> // for MAX_PATH
    #include "winutils.h"
#endif

#include <time.h>
#include <vector>

using namespace std;

WebService::WebService() :
    m_radioMetaDataTimeout( 0 ),
    m_scrobbling( true )
{
    gLogger.Init( savePath( "webservice.log" ), false );
    gLogger.SetLevel( 4 );

    LOGL( 3, "Initialising Web Service" );

    m_radioMetaDataRetryTimer.setSingleShot( true );
    connect( &m_radioMetaDataRetryTimer, SIGNAL( timeout() ),
             this,                       SLOT( requestMetaDataRadio() ) );
             
    m_artistMetadataHttp = new Http( this );
    m_trackMetadataHttp = new Http( this );
}


void
WebService::handshake( const QString& username, const QString& password, const QString& version )
{
    m_username = username;
    m_password = password;

    QString platform = "win32";
    #ifdef Q_WS_X11
    platform = "linux";
    #endif
    #ifdef Q_WS_MAC
    platform = "mac";
    #endif

    QString host = qApp->arguments().contains( "--debug" ) ?
                   "wsdev.audioscrobbler.com" :
                   "ws.audioscrobbler.com";

    Http *http = new Http( host, 80, this );
    connect( http, SIGNAL( responseHeaderReceived( QHttpResponseHeader ) ), this, SLOT( handshakeHeaderReceived( QHttpResponseHeader ) ) );
    connect( http, SIGNAL( errorOccured( int, QString ) ), this, SLOT( handshakeError( int, QString ) ) );
    connect( http, SIGNAL( dataAvailable( const QByteArray& ) ), this, SLOT( handshakeFinished( const QByteArray& ) ) );

    QString path = QString( "/radio/handshake.php?version=%1&platform=%2&username=%3&passwordmd5=%4&language=%5" )
                  .arg( version )
                  .arg( platform )
                  .arg( QString( QUrl::toPercentEncoding( username ) ) )
                  .arg( password )
#ifdef HIDE_RADIO
                  .arg( "jp" );
#else
                  .arg( "en" );
#endif

    http->get( path );
    LOGL( 3, "Radio handshake with: " << host << ", path: " << path );
}


void
WebService::handshakeHeaderReceived( const QHttpResponseHeader &resp )
{
    if ( resp.statusCode() == 503 )
    {
        LOGL( 1, "Handshake Error, status 503" );
        sender()->deleteLater();
    }
}


void
WebService::handshakeError( int error, const QString& errorString )
{
    logHttpError( "Handshake Error", (Http*)sender() );
    emit handshakeResult( -1 );

    sender()->deleteLater();
}


void
WebService::handshakeFinished( const QByteArray& data )
{
    QString result( data );

    //qDebug() << result;

    // TODO: What happens if parsing of any of these fails?
    m_session = parameter( "session", result );
    m_baseHost = parameter( "base_url", result );
    m_basePath = parameter( "base_path", result );
    m_streamUrl.setUrl( parameter( "stream_url", result ) );
    m_subscriber = parameter( "subscriber", result ) == "1";
    bool banned = parameter( "banned", result ) == "1";
    QString infoMsg = parameter( "info_message", result );

    if ( !infoMsg.isEmpty() )
        QMessageBox::information( qApp->activeWindow(), tr( "Last.fm Information" ), infoMsg );

    if ( banned )
    {
        // TODO: make a nicer shutdown (or start upgrade process) via a signal to the container
        QMessageBox::critical( qApp->activeWindow(), tr( "Error" ), tr( "This version is too old and incompatible! Please update!" ) );
        qApp->quit();
    }

    if ( m_session.toLower() == "failed" )
    {
        LOGL( 1, "Radio handshake failed" );

        emit handshakeResult( 0 );
        return;
    }

    LOGL( 3, "Radio handshake successful. Host: " << m_baseHost << ", path: " << m_basePath );

    emit streamingUrl( m_streamUrl );
    emit handshakeResult( m_session.length() == 32 ? 1 : -1 );

    sender()->deleteLater(); // destroy the Http object
}


void
WebService::changeStation( QString url )
{
    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( changeStationFinished( int, bool ) ) );

    if ( url.startsWith( "lastfm://" ) )
    {
        settingsService()->currentUser()->setResumeStation( url );
        url.remove( 0, 9 );
    }

    QString path = QString( m_basePath + "/adjust.php?session=%1&url=lastfm://%2" )
                  .arg( m_session )
                  .arg( url.contains( "%" ) ? url : QString( QUrl::toPercentEncoding( url, "/," ) ) );

    stackAppend( http, http->get( path ) );
    LOGL( 3, m_baseHost << path );

    // This takes care of re-requesting np info when it doesn't refresh so must
    // blank this on station change in case the same track is being played again.
    m_radioSong.clear();

    emit radioStatusChanged( tr( "Tuning into station..." ) );
}


void
WebService::changeStationFinished( int id, bool error )
{
    Http* http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Changing station failed", http );

        QString result( http->readAll() );

        stackRemove( id );
        return;
    }

    QString result( http->readAll() );
    QString resp = parameter( "response", result );
    if ( resp != "OK" )
    {
        int errCode = parameter( "error", result ).toInt();
        QString errorMsg;

        // Ugly!
        switch ( errCode )
        {
            case RADIO_ERROR_NOT_ENOUGH_CONTENT:
            {
                errorMsg = tr( "There is not enough content to play this station." );
            }
            break;

            case RADIO_ERROR_FEWGROUPMEMBERS:
            {
                errorMsg = tr( "This group does not have enough members for radio." );
            }
            break;

            case RADIO_ERROR_FEWFANS:
            {
                errorMsg = tr( "This artist does not have enough fans for radio." );
            }
            break;

            case RADIO_ERROR_UNAVAILABLE:
            {
                errorMsg = tr( "This item is not available for streaming." );
            }
            break;

            case RADIO_ERROR_SUBSCRIBE:
            {
                errorMsg = tr( "This feature is only available to subscribers." );
            }
            break;

            case RADIO_ERROR_FEWNEIGHBOURS:
            {
                errorMsg = tr( "There are not enough neighbours for this radio." );
            }
            break;

            case RADIO_ERROR_OFFLINE:
            {
                errorMsg = tr( "The streaming system is offline for maintenance, please try again later." );
            }
            break;

            default:
            {
                errorMsg = tr( "Starting radio failed. Unknown error." );
            }
        }
        LOGL( 1, "Change station failed: " << errorMsg );

        emit stationChangeFailed( errorMsg );

        // Hack of hacks
        emit radioStatusChanged( "" );
    }
    else
    {
        QString url = parameter( "url", result );
        if ( url.startsWith( "lastfm://" ) )
        {
            m_stationUrl = url;
            QString stationName = parameter( "stationname", result );
            if ( stationName.isEmpty() )
                stationName = url;

            if ( url.startsWith( "play" ) )
                // preview
                emit songQueued();
            else
            {
                // normal station change
                emit skipDone();
                emit stationChanged( url, stationName );
            }

            emit radioStatusChanged( tr( "Getting station info..." ) );
        }
    }

    stackRemove( id );
}


void
WebService::requestMetaDataRadio()
{
    Http  *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( dataAvailable( QByteArray ) ), this, SLOT( metaDataRadioFinished( QByteArray ) ) );

    QString path = QString( m_basePath + "/np.php?session=%1" )
                  .arg( m_session );

    http->get( path );
    LOGL( 3, m_baseHost << path );
    emit trackStatusChanged( tr( "Getting track info..." ) );
}


void
WebService::metaDataRadioFinished( const QByteArray& buffer )
{
    QString stationName;
    QString stationFeed;
    MetaData song;

    if ( buffer.size() <= 0 )
    {
        LOGL( 3, "Get metadata for radio failed" );
        song.populate( m_radioSong );
    }
    else
    {
        QString result( buffer );
        //LOGL( 4, "result: " << result );

        song.setArtist( parameter( "artist", result ) );
        song.setAlbum( parameter( "album", result ) );
        song.setTrack( parameter( "track", result ) );
        song.setAlbumPicUrl( parameter( "albumcover_medium", result ) );
        song.setArtistPageUrl( parameter( "artist_url", result ) );
        song.setAlbumPageUrl( parameter( "album_url", result ) );
        song.setTrackPageUrl( parameter( "track_url", result ) );
        song.setDuration( parameter( "trackduration", result ).toInt() );
        song.setSource( MetaData::Radio );

        int errCode = parameter( "error", result ).toInt(); //FIXME: check for possible error codes
        bool discovery = parameter( "discovery", result ) != "-1";
        stationName = parameter( "station", result );
        stationFeed = parameter( "stationfeed", result );
    }

    if ( song.sameAs( m_radioSong ) )
    {
        LOGL( 3, "Re-requesting metadata" );

        m_radioMetaDataRetryTimer.start( m_radioMetaDataTimeout );

        if ( m_radioMetaDataTimeout > 1000 )
            m_radioMetaDataTimeout += 2500;
        else
            m_radioMetaDataTimeout += 500;
    }
    else
    {
        //LOGL(3, "got new song, populating m_radiosong" );

        m_radioMetaDataTimeout = 0;
        m_radioMetaDataRetryTimer.stop();

        emit metaDataResultRadio( song );
        emit metaDataResultStation( stationName, stationFeed, m_stationName != stationName );

        if ( m_stationName != stationName )
            emit stationNameChanged( m_stationUrl, stationName );

        m_stationName = stationName;
        m_radioSong.populate( song );
    }
}


void
WebService::requestMetaDataArtist( const MetaData& artist )
{
    #ifdef HIDE_RADIO
        QString lang = "jp";
    #else
        QString lang = "en";
    #endif

    QList<QVariant> params;
    params << QVariant( artist.artist() );
    params << QVariant( lang );
    QByteArray xmlData = formatXmlRpc( "artistMetadata", params );

    m_artistMetadataHttp->abort();
    makeXmlRpcCall( xmlData, SLOT( metaDataArtistFinished( QByteArray ) ),
        true, m_artistMetadataHttp );
}


void
WebService::cancelLastMetaDataArtistRequest()
{
    m_artistMetadataHttp->abort();
}


void
WebService::metaDataArtistFinished( const QByteArray& buffer )
{
    // REFACTOR: far too much generic error handling in here, should be
    // abstracted out.

    MetaData data;
    if ( buffer.size() <= 0 )
    {
        LOGL( 2, "Artist metadata request failed, buffer size == 0" );
        emit metaDataResultArtist( data, true );
        return;
    }

    QList<QVariant> retVals;
    QString error;
    bool parsed = parseXmlRpc( buffer, retVals, error );

    if ( !parsed )
    {
        LOGL( 1, error );
        emit metaDataResultArtist( data, true );
        return;
    }

    // There was previously no proper fault struct being returned so we're
    // checking for error (item not found etc) by seeing whether the returned
    // param is a map (struct) or not.
    if ( retVals.at( 0 ).type() != QVariant::Map )
    {
        LOGL( 2, "Result wasn't a <struct>, artist not found?" );
        emit metaDataResultArtist( data, true );
        return;
    }
    
    QMap<QString, QVariant> map = retVals.at( 0 ).toMap();

    if ( map.contains( "faultCode" ) )
    {
        QString faultString = map.value( "faultString" ).toString();
        LOGL( 2, faultString );
        emit metaDataResultArtist( data, true );
        return;
    }

    data.setArtist( map.value( "artistName" ).toString() );
    data.setArtistPageUrl( map.value( "artistPageUrl" ).toString() );

    QStringList tags;
    foreach( QVariant val, map.value( "artistTags" ).toList() )
    {
        tags << val.toString();
    }
    data.setArtistTags( tags );
    data.setNumListeners( map.value( "numListeners" ).toInt() );
    data.setNumPlays( map.value( "numPlays" ).toInt() );
    data.setArtistPicUrl( map.value( "picture" ).toString() );

    QStringList similar;
    foreach( QVariant val, map.value( "similar" ).toList() )
    {
        similar << val.toString();
    }
    data.setSimilarArtists( similar );

    QStringList fans;
    foreach( QVariant val, map.value( "topFans" ).toList() )
    {
        fans << val.toString();
    }
    data.setTopFans( fans );

    data.setWikiPageUrl( map.value( "wikiPageUrl" ).toString() );

    QString wiki = map.value( "wikiText" ).toString();
    CUtils::StripBBCode( wiki );
    data.setWiki( wiki );

    emit metaDataResultArtist( data, false );
}


void
WebService::requestMetaDataTrack( const MetaData& track )
{
    #ifdef HIDE_RADIO
        QString lang = "jp";
    #else
        QString lang = "en";
    #endif

    QList<QVariant> params;
    params << QVariant( track.artist() );
    params << QVariant( track.track() );
    params << QVariant( track.album() );
    params << QVariant( lang );
    QByteArray xmlData = formatXmlRpc( "trackMetadata", params );

    m_trackMetadataHttp->abort();
    makeXmlRpcCall( xmlData, SLOT( metaDataTrackFinished( QByteArray ) ),
        true, m_trackMetadataHttp );
}


void
WebService::cancelLastMetaDataTrackRequest()
{
    m_trackMetadataHttp->abort();
}


void
WebService::metaDataTrackFinished( const QByteArray& buffer )
{
    // REFACTOR: far too much generic error handling in here, should be
    // abstracted out.

    MetaData data;
    if ( buffer.size() <= 0 )
    {
        LOGL( 2, "Track metadata request failed, buffer size == 0" );
        emit metaDataResultTrack( data, true );
        return;
    }

    QList<QVariant> retVals;
    QString error;
    bool parsed = parseXmlRpc( buffer, retVals, error );

    if ( !parsed )
    {
        LOGL( 1, error );
        emit metaDataResultTrack( data, true );
        return;
    }

    // There was previously no proper fault struct being returned so we're
    // checking for error (item not found etc) by seeing whether the returned
    // param is a map (struct) or not.
    if ( retVals.at( 0 ).type() != QVariant::Map )
    {
        LOGL( 2, "Result wasn't a <struct>, track not found?" );
        emit metaDataResultTrack( data, true );
        return;
    }

    QMap<QString, QVariant> map = retVals.at( 0 ).toMap();

    if ( map.contains( "faultCode" ) )
    {
        QString faultString = map.value( "faultString" ).toString();
        LOGL( 2, faultString );
        emit metaDataResultTrack( data, true );
        return;
    }

    data.setArtist( map.value( "artistName" ).toString() );
    data.setAlbumPicUrl( map.value( "albumCover" ).toString() );
    data.setLabel( map.value( "albumLabel" ).toString() );
    data.setAlbum( map.value( "albumName" ).toString() );
    data.setNumTracks( map.value( "albumNumTracks" ).toInt() );
    QString dateStr = map.value( "albumReleaseDate" ).toString();
    data.setReleaseDate( QDate::fromString( dateStr, Qt::ISODate ) );
    data.setAlbumPageUrl( map.value( "albumUrl" ).toString() );
    data.setBuyTrackUrl( map.value( "trackBuyURL" ).toString() );
    data.setBuyTrackString( map.value( "buyTrackString" ).toString() );
    data.setBuyAlbumUrl( map.value( "buyAlbumURL" ).toString() );
    data.setBuyAlbumString( map.value( "buyAlbumString" ).toString() );

    QStringList tags;
    foreach( QVariant val, map.value( "trackTags" ).toList() )
    {
        tags << val.toString();
    }
    data.setTrackTags( tags );

    data.setTrack( map.value( "trackTitle" ).toString() );
    data.setTrackPageUrl( map.value( "trackUrl" ).toString() );

    emit metaDataResultTrack( data, false );

}


void
WebService::enableScrobbling( bool enabled, bool forceWebReq )
{

    if ( enabled == m_scrobbling && !forceWebReq )
        return; // no need to re-set an existing state
    else
        m_scrobbling = enabled;

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( enableScrobblingFinished( int, bool ) ) );

    QString path = QString( m_basePath + "/control.php?session=%1&command=%2" )
                  .arg( m_session )
                  .arg( enabled ? QString( "rtp" ) : QString( "nortp" ) );

    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::enableScrobblingFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Enable scrobbling call failed", http );
        stackRemove( id );
        return;
    }

    stackRemove( id );
    emit enableScrobblingDone();
}


void
WebService::enableDiscoveryMode( bool enabled )
{
    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( enableDiscoveryModeFinished( int, bool ) ) );

    QString path = QString( m_basePath + "/adjust.php?session=%1&url=lastfm://settings/discovery/%2" )
                      .arg( m_session )
                      .arg( enabled ? "on" : "off" );

    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::enableDiscoveryModeFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Enable discovery mode call failed", http );
        stackRemove( id );
        return;
    }

    stackRemove( id );
    emit enableDiscoveryModeDone();
}


void
WebService::unlisten( QString artist, QString track )
{
    clearActionCache();
    m_actionArtist = artist;
    m_actionTrack  = track;

    QString challenge = challengeString();
    QString xmlData;

    xmlData = QString( "<?xml version=\"1.0\"?>"
                            "<methodCall>"
                                "<methodName>removeRecentlyListenedTrack</methodName>"
                                "<params>"
                                    "<param><value><string>%1</string></value></param>"
                                    "<param><value><string>%2</string></value></param>"
                                    "<param><value><string>%3</string></value></param>"
                                    "<param><value><string>%4</string></value></param>"
                                    "<param><value><string>%5</string></value></param>"
                                "</params>"
                            "</methodCall>" )
                    .arg( xmlEncode( currentUsername() ) )
                    .arg( QString( challenge ) )
                    .arg( QString( MD5Digest( QString( currentPassword() + challenge ).toUtf8() ) ) )
                    .arg( xmlEncode( artist ) )
                    .arg( xmlEncode( track ) );

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( unlistenFinished( int, bool ) ) );

    QHttpRequestHeader header( "POST", "/1.0/rw/xmlrpc.php" );
    header.setValue( "Host", m_baseHost );
    header.setContentType( "text/xml" );
    stackAppend( http, http->request( header, xmlData.toUtf8() ) );

    LOGL( 3, m_baseHost << "\n" << xmlData );
}


void
WebService::love( QString artist, QString track )
{
    clearActionCache();
    m_actionArtist = artist;
    m_actionTrack  = track;

    QString challenge = challengeString();
    QString xmlData;

    xmlData = QString( "<?xml version=\"1.0\"?>"
                            "<methodCall>"
                                "<methodName>loveTrack</methodName>"
                                "<params>"
                                    "<param><value><string>%1</string></value></param>"
                                    "<param><value><string>%2</string></value></param>"
                                    "<param><value><string>%3</string></value></param>"
                                    "<param><value><string>%4</string></value></param>"
                                    "<param><value><string>%5</string></value></param>"
                                "</params>"
                            "</methodCall>" )
                    .arg( xmlEncode( currentUsername() ) )
                    .arg( QString( challenge ) )
                    .arg( QString( MD5Digest( QString( currentPassword() + challenge ).toUtf8() ) ) )
                    .arg( xmlEncode( artist ) )
                    .arg( xmlEncode( track ) );

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( loveFinished( int, bool ) ) );

    QHttpRequestHeader header( "POST", "/1.0/rw/xmlrpc.php" );
    header.setValue( "Host", m_baseHost );
    header.setContentType( "text/xml" );
    stackAppend( http, http->request( header, xmlData.toUtf8() ) );

    LOGL( 3, m_baseHost << "\n" << xmlData );
}


void
WebService::unlove( QString artist, QString track )
{
    clearActionCache();
    m_actionArtist = artist;
    m_actionTrack  = track;

    QString challenge = challengeString();
    QString xmlData;

    xmlData = QString( "<?xml version=\"1.0\"?>"
                            "<methodCall>"
                                "<methodName>unLoveTrack</methodName>"
                                "<params>"
                                    "<param><value><string>%1</string></value></param>"
                                    "<param><value><string>%2</string></value></param>"
                                    "<param><value><string>%3</string></value></param>"
                                    "<param><value><string>%4</string></value></param>"
                                    "<param><value><string>%5</string></value></param>"
                                "</params>"
                            "</methodCall>" )
                    .arg( xmlEncode( currentUsername() ) )
                    .arg( QString( challenge ) )
                    .arg( QString( MD5Digest( QString( currentPassword() + challenge ).toUtf8() ) ) )
                    .arg( xmlEncode( artist ) )
                    .arg( xmlEncode( track ) );

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( unloveFinished( int, bool ) ) );

    QHttpRequestHeader header( "POST", "/1.0/rw/xmlrpc.php" );
    header.setValue( "Host", m_baseHost );
    header.setContentType( "text/xml" );
    stackAppend( http, http->request( header, xmlData.toUtf8() ) );

    LOGL( 3, m_baseHost << "\n" << xmlData );
}


void
WebService::ban( QString artist, QString track )
{
    clearActionCache();
    m_actionArtist = artist;
    m_actionTrack  = track;

    QString challenge = challengeString();
    QString xmlData;

    xmlData = QString( "<?xml version=\"1.0\"?>"
                            "<methodCall>"
                                "<methodName>banTrack</methodName>"
                                "<params>"
                                    "<param><value><string>%1</string></value></param>"
                                    "<param><value><string>%2</string></value></param>"
                                    "<param><value><string>%3</string></value></param>"
                                    "<param><value><string>%4</string></value></param>"
                                    "<param><value><string>%5</string></value></param>"
                                "</params>"
                            "</methodCall>" )
                    .arg( xmlEncode( currentUsername() ) )
                    .arg( QString( challenge ) )
                    .arg( QString( MD5Digest( QString( currentPassword() + challenge ).toUtf8() ) ) )
                    .arg( xmlEncode( artist ) )
                    .arg( xmlEncode( track ) );

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( banFinished( int, bool ) ) );

    QHttpRequestHeader header( "POST", "/1.0/rw/xmlrpc.php" );
    header.setValue( "Host", m_baseHost );
    header.setContentType( "text/xml" );
    stackAppend( http, http->request( header, xmlData.toUtf8() ) );

    LOGL( 3, m_baseHost << "\n" << xmlData );
}


void
WebService::unban( QString artist, QString track )
{
    clearActionCache();
    m_actionArtist = artist;
    m_actionTrack  = track;

    QString challenge = challengeString();
    QString xmlData;

    xmlData = QString( "<?xml version=\"1.0\"?>"
                            "<methodCall>"
                                "<methodName>unBanTrack</methodName>"
                                "<params>"
                                    "<param><value><string>%1</string></value></param>"
                                    "<param><value><string>%2</string></value></param>"
                                    "<param><value><string>%3</string></value></param>"
                                    "<param><value><string>%4</string></value></param>"
                                    "<param><value><string>%5</string></value></param>"
                                "</params>"
                            "</methodCall>" )
                    .arg( xmlEncode( currentUsername() ) )
                    .arg( QString( challenge ) )
                    .arg( QString( MD5Digest( QString( currentPassword() + challenge ).toUtf8() ) ) )
                    .arg( xmlEncode( artist ) )
                    .arg( xmlEncode( track ) );

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( unbanFinished( int, bool ) ) );

    QHttpRequestHeader header( "POST", "/1.0/rw/xmlrpc.php" );
    header.setValue( "Host", m_baseHost );
    header.setContentType( "text/xml" );
    stackAppend( http, http->request( header, xmlData.toUtf8() ) );

    LOGL( 3, m_baseHost << "\n" << xmlData );
}


void
WebService::skip()
{
    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( skipFinished( int, bool ) ) );

    QString path = QString( m_basePath + "/control.php?session=%1&command=skip" )
                  .arg( m_session );

    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );

    emit trackStatusChanged( tr( "Skipping..." ) );
}


void
WebService::unlistenFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "UnListen request failed", http );
        stackRemove( id );
        return;
    }

    stackRemove( id );
    emit unlistenDone( m_actionArtist, m_actionTrack );
}


void
WebService::loveFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Love request failed", http );
        stackRemove( id );
        return;
    }

    stackRemove( id );
    emit loveDone( m_actionArtist, m_actionTrack );
}


void
WebService::unloveFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "UnLove request failed", http );
        stackRemove( id );
        return;
    }

    stackRemove( id );
    emit unloveDone( m_actionArtist, m_actionTrack );
}


void
WebService::banFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Ban request failed", http );
        stackRemove( id );
        return;
    }

    stackRemove( id );
    emit banDone( m_actionArtist, m_actionTrack );
}


void
WebService::unbanFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "UnBan request failed", http );
        stackRemove( id );
        return;
    }

    stackRemove( id );
    emit unbanDone( m_actionArtist, m_actionTrack );
}


void
WebService::skipFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Skip request failed", http );
        stackRemove( id );
        return;
    }

    stackRemove( id );
    emit skipDone();
}


void
WebService::friends( QString username )
{
    if ( username.isEmpty() )
        username = m_username;

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( friendsFinished( int, bool ) ) );


    QString path = QString( "/1.0/user/" + CUtils::UrlEncodeItem( username ) + "/friends.xml?showtracks=1" );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::friendsFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Retrieve friends request failed", http );
        stackRemove( id );
        return;
    }

    QDomDocument document;
    document.setContent( http->readAll() );

    if ( document.elementsByTagName( "friends" ).length() == 0 )
    {
        stackRemove( id );
        emit friendsResult( QString( "" ), QStringList() );
        return;
    }

    QList<UserMetaData> friendsList;
    QStringList friends;

    QString user = document.elementsByTagName( "friends" ).item( 0 ).attributes().namedItem( "user" ).nodeValue();
    QDomNodeList values = document.elementsByTagName( "user" );
    for ( int i = 0; i < values.count(); i++ )
    {
        UserMetaData umd;
        QDomNode image = values.item( i ).namedItem( "image" );
        umd.setName( values.item( i ).attributes().namedItem( "username" ).nodeValue() );

        if ( !image.isNull() )
            umd.setImage( QUrl( image.toElement().text() ) );

        // recent track
        QDomNode lasttrack = values.item( i ).namedItem( "lasttrack" );
        if ( !lasttrack.isNull() )
        {
            QDomNode recentArtist = lasttrack.namedItem( "artist" );
            QDomNode recentTrack = lasttrack.namedItem( "name" );
            QDomNode recentDate = lasttrack.namedItem( "date" );

            if ( !recentArtist.isNull() && !recentTrack.isNull() && !recentDate.isNull() )
            {
                umd.setLastActivity( recentDate.toElement().text() );
                umd.setRecentTrack( QStringList( QString( "%1 - %2" )
                                                    .arg( recentArtist.toElement().text() )
                                                    .arg( recentTrack.toElement().text() ) ) );
            }
        }

        friendsList << umd;
        friends << values.item( i ).attributes().namedItem( "username" ).nodeValue();
    }

    stackRemove( id );
    emit friendsResult( user, friendsList );
    emit friendsResult( user, friends );
}


void
WebService::neighbours( QString username )
{
    if ( username.isEmpty() )
        username = m_username;

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( neighboursFinished( int, bool ) ) );

    QString path = QString( "/1.0/user/" + CUtils::UrlEncodeItem( username ) + "/neighbours.xml" );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::neighboursFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Retrieve neighbours request failed", http );

        stackRemove( id );
        return;
    }

    QByteArray b = http->readAll();
    //qDebug() << b;
    
    QDomDocument document;
    document.setContent( b );

    QList<QPair<QString, int> > neighbours;

    if ( document.elementsByTagName( "neighbours" ).length() == 0 )
    {
        stackRemove( id );
        emit neighboursResult( QString(), neighbours );
        return;
    }

    QString user = document.elementsByTagName( "neighbours" ).item( 0 ).attributes().namedItem( "user" ).nodeValue();
    QDomNodeList values = document.elementsByTagName( "user" );
    for ( int i = 0; i < values.count(); i++ )
    {
        QString name = values.item( i ).attributes().namedItem( "username" ).nodeValue();

        QDomNode node = values.item( i ).namedItem( "match" );
        int match = node.toElement().text().toFloat();
        
        neighbours << qMakePair( name, match );
    }

    stackRemove( id );
    emit neighboursResult( user, neighbours );
}


void
WebService::artistTags( QString artist )
{
    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( artistTagsFinished( int, bool ) ) );

    QString path = QString( "/1.0/artist/" + CUtils::UrlEncodeItem( artist ) + "/toptags.xml" );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::artistTagsFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        stackRemove( id );
        return;
    }

    QDomDocument document;
    document.setContent( http->readAll() );

    QStringList tags;
    if ( document.elementsByTagName( "toptags" ).length() == 0 )
    {
        stackRemove( id );
        emit artistTagsResult( "", tags );
        return;
    }

    QDomNodeList values = document.elementsByTagName( "tag" );
    for ( int i = 0; i < values.count(); i++ )
    {
        QDomNode item = values.item( i ).namedItem( "name" );
        tags << item.toElement().text();
    }

    stackRemove( id );
    emit artistTagsResult( "", tags );
}


void
WebService::topTags()
{
    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( topTagsFinished( int, bool ) ) );

    QString path = "/1.0/tag/toptags.xml";
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::topTagsFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        stackRemove( id );
        return;
    }

    QDomDocument document;
    document.setContent( http->readAll() );

    QList<QPair<QString, int> > tags;

    if ( document.elementsByTagName( "toptags" ).length() == 0 )
    {
        stackRemove( id );
        emit topTagsResult( tags );
        return;
    }

    //QString user = document.elementsByTagName( "toptags" ).item( 0 ).attributes().namedItem( "tag" ).nodeValue();
    QDomNodeList values = document.elementsByTagName( "tag" );
    for ( int i = 0; i < values.count(); i++ )
    {
        QString name = values.item( i ).attributes().namedItem( "name" ).nodeValue();
        int count = values.item( i ).attributes().namedItem( "count" ).nodeValue().toInt();
        tags << qMakePair( name, count );
    }

    stackRemove( id );
    emit topTagsResult( tags );
}


void
WebService::userTags( QString username )
{
    if ( username.isEmpty() )
        username = m_username;

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( userTagsFinished( int, bool ) ) );

    QString path = QString( "/1.0/user/" +  CUtils::UrlEncodeItem( username ) + "/tags.xml" );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::userTagsFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Retrieve user tags request failed", http );
        stackRemove( id );
        return;
    }

    QDomDocument document;
    document.setContent( http->readAll() );

    QList<QPair<QString, int> > tags;

    if ( document.elementsByTagName( "toptags" ).length() == 0 )
    {
        stackRemove( id );
        emit userTagsResult( QString(), tags );
        return;
    }

    QDomNodeList values = document.elementsByTagName( "tag" );
    QString user = document.elementsByTagName( "toptags" ).item( 0 ).attributes().namedItem( "user" ).nodeValue();
    for ( int i = 0; i < values.count(); i++ )
    {
        QString name = values.item( i ).namedItem( "name" ).toElement().text();
        int count = values.item( i ).namedItem( "count" ).toElement().text().toInt();
        tags << qMakePair( name, count );
    }

    stackRemove( id );
    emit userTagsResult( user, tags );
}


void
WebService::userArtistTags( QString artist, QString username )
{
    if ( username.isEmpty() )
        username = m_username;

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( userArtistTagsFinished( int, bool ) ) );

    QString path = "/1.0/user/" + CUtils::UrlEncodeItem( username ) +
        "/artisttags.xml?artist=" + QUrl::toPercentEncoding( artist );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::userAlbumTags( QString artist, QString album, QString username )
{
    if ( username.isEmpty() )
        username = m_username;

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( userAlbumTagsFinished( int, bool ) ) );

    QString path = "/1.0/user/" + CUtils::UrlEncodeItem( username ) +
        "/albumtags.xml?artist=" + QUrl::toPercentEncoding( artist ) +
        "&album=" + QUrl::toPercentEncoding( album );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::userTrackTags( QString artist, QString track, QString username )
{
    if ( username.isEmpty() )
        username = m_username;

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( userTrackTagsFinished( int, bool ) ) );

    QString path = "/1.0/user/" + CUtils::UrlEncodeItem( username ) +
        "/tracktags.xml?artist=" + QUrl::toPercentEncoding( artist ) +
        "&track=" + QUrl::toPercentEncoding( track );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::userArtistTagsFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        stackRemove( id );
        return;
    }

    QDomDocument document;
    document.setContent( http->readAll() );

    QStringList tags;
    if ( document.elementsByTagName( "artisttags" ).length() == 0 )
    {
        stackRemove( id );
        emit userArtistTagsResult( m_username, tags );
        return;
    }

    QDomNodeList values = document.elementsByTagName( "tag" );
    for ( int i = 0; i < values.count(); i++ )
    {
        QDomNode item = values.item( i ).namedItem( "name" );
        tags << item.toElement().text();
    }

    stackRemove( id );
    emit userArtistTagsResult( m_username, tags );
}


void
WebService::userTrackTagsFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        stackRemove( id );
        return;
    }

    QDomDocument document;
    document.setContent( http->readAll() );

    QStringList tags;
    if ( document.elementsByTagName( "tracktags" ).length() == 0 )
    {
        stackRemove( id );
        emit userTrackTagsResult( m_username, tags );
        return;
    }

    QDomNodeList values = document.elementsByTagName( "tag" );
    for ( int i = 0; i < values.count(); i++ )
    {
        QDomNode item = values.item( i ).namedItem( "name" );
        tags << item.toElement().text();
    }

    stackRemove( id );
    emit userTrackTagsResult( m_username, tags );
}


void
WebService::userAlbumTagsFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        stackRemove( id );
        return;
    }

    QDomDocument document;
    document.setContent( http->readAll() );

    QStringList tags;
    if ( document.elementsByTagName( "albumtags" ).length() == 0 )
    {
        stackRemove( id );
        emit userAlbumTagsResult( m_username, tags );
        return;
    }

    QDomNodeList values = document.elementsByTagName( "tag" );
    for ( int i = 0; i < values.count(); i++ )
    {
        QDomNode item = values.item( i ).namedItem( "name" );
        tags << item.toElement().text();
    }

    stackRemove( id );
    emit userAlbumTagsResult( m_username, tags );
}


void
WebService::userPics( QStringList names )
{
    QList<QVariant> params;
    params << QVariant( names );
    QByteArray xmlData = formatXmlRpc( "getAvatarUrls", params );

    makeXmlRpcCall( xmlData, SLOT( userPicsFinished( QByteArray ) ) );
}

void
WebService::userPicsFinished( QByteArray buffer )
{
    sender()->deleteLater();

    //void userPicsResult( const QList<QPair<QString, QString> >& names );

    QMap<QString, QString> urls;
    if ( buffer.size() <= 0 )
    {
        LOGL( 2, "User pics request failed, buffer size == 0" );
        emit userPicsResult( urls, true );
        return;
    }
    
    QList<QVariant> retVals;
    QString error;
    bool parsed = parseXmlRpc( buffer, retVals, error );

    if ( !parsed )
    {
        LOGL( 1, error );
        emit userPicsResult( urls, true );
        return;
    }

    if ( retVals.at( 0 ).type() != QVariant::List )
    {
        LOGL( 1, "Result wasn't an <array>." );
        emit userPicsResult( urls, true );
        return;
    }

    QList<QVariant> array = retVals.at( 0 ).toList();

    foreach( QVariant val, array )
    {
        QMap<QString, QVariant> map = val.toMap();

        QString user = map.value( "name" ).toString();
        QString url = map.value( "avatar" ).toString();
        urls.insert( user, url );
    }
    
    emit userPicsResult( urls, false );
}

void
WebService::recentTracks( QString username )
{
    if ( username.isEmpty() )
        username = m_username;

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( recentTracksFinished( int, bool ) ) );

    QString path = QString( "/1.0/user/" + CUtils::UrlEncodeItem( username ) + "/recenttracks.xml" );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::recentTracksFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Retrieve recent tracks request failed", http );

        stackRemove( id );
        return;
    }

    QList< QPair<QString, QString> > songs;
    QDomDocument document;
    document.setContent( http->readAll() );

    if ( document.elementsByTagName( "recenttracks" ).length() == 0 )
    {
        stackRemove( id );
        emit recentTracksResult( QString(), songs );
        return;
    }

    QDomNodeList values = document.elementsByTagName( "track" );
    QString user = document.elementsByTagName( "recenttracks" ).item( 0 ).attributes().namedItem( "user" ).nodeValue();
    for ( int i = 0; i < values.count(); i++ )
    {
        QPair<QString, QString> song;
        song.first = values.item( i ).namedItem( "artist" ).toElement().text();
        song.second = values.item( i ).namedItem( "name" ).toElement().text();

        songs << song;
    }

    stackRemove( id );
    emit recentTracksResult( user, songs );
}


void
WebService::recentLovedTracks( QString username )
{
    if ( username.isEmpty() )
        username = m_username;

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( recentLovedTracksFinished( int, bool ) ) );

    QString path = QString( "/1.0/user/" + CUtils::UrlEncodeItem( username ) + "/recentlovedtracks.xml" );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::recentLovedTracksFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Retrieve recent loved tracks request failed", http );

        stackRemove( id );
        return;
    }

    QList< QPair<QString, QString> > songs;
    QDomDocument document;
    document.setContent( http->readAll() );

    if ( document.elementsByTagName( "recentlovedtracks" ).length() == 0 )
    {
        stackRemove( id );
        emit recentTracksResult( QString(), songs );
        return;
    }

    QDomNodeList values = document.elementsByTagName( "track" );
    QString user = document.elementsByTagName( "recentlovedtracks" ).item( 0 ).attributes().namedItem( "user" ).nodeValue();
    for ( int i = 0; i < values.count(); i++ )
    {
        QPair<QString, QString> song;
        song.first = values.item( i ).namedItem( "artist" ).toElement().text();
        song.second = values.item( i ).namedItem( "name" ).toElement().text();

        songs << song;
    }

    stackRemove( id );
    emit recentLovedTracksResult( user, songs );
}


void
WebService::recentBannedTracks( QString username )
{
    if ( username.isEmpty() )
        username = m_username;

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( recentBannedTracksFinished( int, bool ) ) );

    QString path = QString( "/1.0/user/" + CUtils::UrlEncodeItem( username ) + "/recentbannedtracks.xml" );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}


void
WebService::recentBannedTracksFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Retrieve recent banned tracks request failed", http );

        stackRemove( id );
        return;
    }

    QList< QPair<QString, QString> > songs;
    QDomDocument document;
    document.setContent( http->readAll() );

    if ( document.elementsByTagName( "recentbannedtracks" ).length() == 0 )
    {
        stackRemove( id );
        emit recentTracksResult( QString(), songs );
        return;
    }

    QDomNodeList values = document.elementsByTagName( "track" );
    QString user = document.elementsByTagName( "recentbannedtracks" ).item( 0 ).attributes().namedItem( "user" ).nodeValue();
    for ( int i = 0; i < values.count(); i++ )
    {
        QPair<QString, QString> song;
        song.first = values.item( i ).namedItem( "artist" ).toElement().text();
        song.second = values.item( i ).namedItem( "name" ).toElement().text();

        songs << song;
    }

    stackRemove( id );
    emit recentBannedTracksResult( user, songs );
}


void
WebService::similarArtists( QString artist )
{
    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( similarArtistsFinished( int, bool ) ) );

    QString path = QString( "/1.0/get.php?resource=artist&document=similar&format=xml&artist=" +
        QUrl::toPercentEncoding( artist ) );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}

void
WebService::similarArtistsFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Search artist request failed", http );

        stackRemove( id );
        return;
    }

    QDomDocument document;
    document.setContent( http->readAll() );

    QList<QPair<QString, int> > artists;

    if ( document.elementsByTagName( "similarartists" ).length() == 0 )
    {
        stackRemove( id );
        emit similarArtistsResult( artists );
        return;
    }

    QStringList images;
    QString artist  = document.elementsByTagName( "similarartists" ).item( 0 ).attributes().namedItem( "artist" ).nodeValue();
    QString image   = document.elementsByTagName( "similarartists" ).item( 0 ).attributes().namedItem( "picture" ).nodeValue();
    bool streamable = ( document.elementsByTagName( "similarartists" ).item( 0 ).attributes().namedItem( "streamable" ).nodeValue() == "1" );

    artists << qMakePair( artist, 100 );
    QDomNodeList values = document.elementsByTagName( "artist" );
    for ( int i = 0; i < values.count(); i++ )
    {
        QDomNode item = values.item( i ).namedItem( "name" );
        QDomNode match = values.item( i ).namedItem( "match" );
        artists << qMakePair( item.toElement().text(),
                              match.toElement().text().toInt() );

        QDomNode image = values.item( i ).namedItem( "image_small" );
        images << image.toElement().text();
    }

    stackRemove( id );
//     emit similarArtistsResult( artist, image, streamable, artists, images );
    emit similarArtistsResult( artists, artist );
}

void
WebService::searchTag( QString tag )
{
    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( searchTagFinished( int, bool ) ) );

    QString path = QString( "/1.0/tag/" + CUtils::UrlEncodeItem( tag ) + "/search.xml?showtop10=1" );
    stackAppend( http, http->get( path ) );

    LOGL( 3, m_baseHost << path );
}

void
WebService::searchTagFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Search tag request failed", http );

        stackRemove( id );
        return;
    }

    QDomDocument document;
    document.setContent( http->readAll() );

    QList<QPair<QString, int> > tags;
    QList<QStringList> topArtistList;
    if ( document.elementsByTagName( "tags" ).length() == 0 )
    {
        stackRemove( id );
        emit searchTagResult( tags );
        return;
    }

    QDomNodeList values = document.elementsByTagName( "tag" );
    for ( int i = 0; i < values.count(); i++ )
    {
        QDomNode item = values.item( i ).namedItem( "name" );
        QDomNode match = values.item( i ).namedItem( "match" );
        int matchPerc = match.toElement().text().toFloat() * 100;
        tags << qMakePair( item.toElement().text(),
                           matchPerc );

        QStringList topArtists;
        QDomElement topartistsNode = values.item( i ).firstChildElement( "topartists" );
        if ( !topartistsNode.isNull() )
        {
            QDomNodeList artistsNode = topartistsNode.elementsByTagName( "artist" );
            for ( int x = 0; x < artistsNode.count(); x++ )
            {
                topArtists << artistsNode.item( x ).toElement().text();
            }
        }

        topArtistList.append( topArtists );
    }
    stackRemove( id );
    emit searchTagResult( tags );
}


void
WebService::similarTags( QString tag )
{
    QList<QVariant> params;
    params << QVariant( tag );
    QByteArray xmlData = formatXmlRpc( "getSimilarTags", params );

    makeXmlRpcCall( xmlData, SLOT( similarTagsFinished( QByteArray ) ) );
}

void
WebService::similarTagsFinished( QByteArray buffer )
{
    // REFACTOR: far too much generic error handling in here, should be
    // abstracted out.

    sender()->deleteLater();

    QList<QPair<QString, int> > tags;
    if ( buffer.size() <= 0 )
    {
        LOGL( 2, "Similar tag request failed, buffer size == 0" );
        emit similarTagsResult( tags );
        return;
    }

    QList<QVariant> retVals;
    QString error;
    bool parsed = parseXmlRpc( buffer, retVals, error );

    if ( !parsed || retVals.isEmpty() )
    {
        LOGL( 1, error );
        emit similarTagsResult( tags );
        return;
    }

    if ( retVals[0].type() != QVariant::Map )
    {
        // No tags found
        emit similarTagsResult( tags );
        return;
    }

    QMap<QString, QVariant> topMap = retVals.at( 0 ).toMap();

    if ( topMap.contains( "faultCode" ) )
    {
        QString faultString = topMap.value( "faultString" ).toString();
        LOGL( 2, faultString );
        emit similarTagsResult( tags );
        return;
    }

    QString searchTerm = topMap.value( "search" ).toString();

    tags << qMakePair( searchTerm, 0 ); // will update weight further down

    QList<QVariant> list = topMap.value( "tags" ).toList();
    int maxWeight = 0;
    foreach( QVariant val, list )
    {
        QMap<QString, QVariant> map = val.toMap();
        QString tagName = map.value( "name" ).toString();
        int tagWeight = map.value( "weight" ).toInt();

        if ( tagWeight > maxWeight )
        {
            maxWeight = tagWeight;
        }

        tags << qMakePair( tagName.toLower(), tagWeight );
    }

    // Give the search term the max weight
    tags[0].second = qMax( 1, maxWeight );

    emit similarTagsResult( tags );
}

void
WebService::recommend( int type, QString username, QString message, QString artist, QString token )
{
    clearActionCache();
    m_actionArtist = artist;

    QString challenge = challengeString();
    QString xmlData;

    switch ( type )
    {
        case ItemArtist:
            xmlData = QString( "<?xml version=\"1.0\"?>"
                                    "<methodCall>"
                                        "<methodName>recommendArtist</methodName>"
                                        "<params>"
                                            "<param><value><string>%1</string></value></param>"
                                            "<param><value><string>%2</string></value></param>"
                                            "<param><value><string>%3</string></value></param>"
                                            "<param><value><string>%4</string></value></param>"
                                            "<param><value><string>%5</string></value></param>"
                                            "<param><value><string>%6</string></value></param>"
                                        "</params>"
                                    "</methodCall>" )
                            .arg( xmlEncode( currentUsername() ) )
                            .arg( QString( challenge ) )
                            .arg( QString( MD5Digest( QString( currentPassword() + challenge ).toUtf8() ) ) )
                            .arg( xmlEncode( artist ) )
                            .arg( xmlEncode( username ) )
                            .arg( xmlEncode( message ) );
            break;

        case ItemAlbum:
            m_actionAlbum = token;
            xmlData = QString( "<?xml version=\"1.0\"?>"
                                    "<methodCall>"
                                        "<methodName>recommendAlbum</methodName>"
                                        "<params>"
                                            "<param><value><string>%1</string></value></param>"
                                            "<param><value><string>%2</string></value></param>"
                                            "<param><value><string>%3</string></value></param>"
                                            "<param><value><string>%4</string></value></param>"
                                            "<param><value><string>%5</string></value></param>"
                                            "<param><value><string>%6</string></value></param>"
                                            "<param><value><string>%7</string></value></param>"
                                        "</params>"
                                    "</methodCall>" )
                            .arg( xmlEncode( currentUsername() ) )
                            .arg( QString( challenge ) )
                            .arg( QString( MD5Digest( QString( currentPassword() + challenge ).toUtf8() ) ) )
                            .arg( xmlEncode( artist ) )
                            .arg( xmlEncode( token ) )
                            .arg( xmlEncode( username ) )
                            .arg( xmlEncode( message ) );
            break;

        case ItemTrack:
            m_actionTrack = token;
            xmlData = QString( "<?xml version=\"1.0\"?>"
                                    "<methodCall>"
                                        "<methodName>recommendTrack</methodName>"
                                        "<params>"
                                            "<param><value><string>%1</string></value></param>"
                                            "<param><value><string>%2</string></value></param>"
                                            "<param><value><string>%3</string></value></param>"
                                            "<param><value><string>%4</string></value></param>"
                                            "<param><value><string>%5</string></value></param>"
                                            "<param><value><string>%6</string></value></param>"
                                            "<param><value><string>%7</string></value></param>"
                                        "</params>"
                                    "</methodCall>" )
                            .arg( xmlEncode( currentUsername() ) )
                            .arg( QString( challenge ) )
                            .arg( QString( MD5Digest( QString( currentPassword() + challenge ).toUtf8() ) ) )
                            .arg( xmlEncode( artist ) )
                            .arg( xmlEncode( token ) )
                            .arg( xmlEncode( username ) )
                            .arg( xmlEncode( message ) );
            break;
    }

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( recommendFinished( int, bool ) ) );

    QHttpRequestHeader header( "POST", "/1.0/rw/xmlrpc.php" );
    header.setValue( "Host", m_baseHost );
    header.setContentType( "text/xml" );
    stackAppend( http, http->request( header, xmlData.toUtf8() ) );

    LOGL( 3, m_baseHost << "\n" << xmlData );
}


void
WebService::recommendFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Recommend request failed", http );
    }

    QString result( http->readAll() );

    stackRemove( id );
    emit recommendDone( m_actionArtist, m_actionAlbum, m_actionTrack );
}


void
WebService::deleteFriend( QString user )
{
    clearActionCache();
    m_actionUser = user;

    QString challenge = challengeString();
    QString xmlData;

    xmlData = QString( "<?xml version=\"1.0\"?>"
                            "<methodCall>"
                                "<methodName>removeFriend</methodName>"
                                "<params>"
                                    "<param><value><string>%1</string></value></param>"
                                    "<param><value><string>%2</string></value></param>"
                                    "<param><value><string>%3</string></value></param>"
                                    "<param><value><string>%4</string></value></param>"
                                "</params>"
                            "</methodCall>" )
                    .arg( xmlEncode( currentUsername() ) )
                    .arg( QString( challenge ) )
                    .arg( QString( MD5Digest( QString( currentPassword() + challenge ).toUtf8() ) ) )
                    .arg( xmlEncode( user ) );

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( deleteFriendFinished( int, bool ) ) );

    QHttpRequestHeader header( "POST", "/1.0/rw/xmlrpc.php" );
    header.setValue( "Host", m_baseHost );
    header.setContentType( "text/xml" );
    stackAppend( http, http->request( header, xmlData.toUtf8() ) );

    LOGL( 3, m_baseHost << "\n" << xmlData );

}


void
WebService::deleteFriendFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Delete friend request failed", http );
    }

    QString result( http->readAll() );

    stackRemove( id );
    emit deleteFriendDone( m_actionUser );
}


void
WebService::tagItem( int type, int mode, QString tag, QString artist, QString token )
{
    clearActionCache();
    m_actionArtist = artist;

    QString challenge = challengeString();
    QByteArray xmlData;

    QStringList tags = tag.split( "," );
    for ( int i = 0; i < tags.count(); i++ )
    {
        tags[i] = tags.at( i ).trimmed();
    }
    QString auth = MD5Digest( QString( currentPassword() + challenge ).toUtf8() );
    QString modeParam = mode == TAG_OVERWRITE ? "set" : "append";

    switch ( type )
    {
        case ItemArtist:
        {
            QList<QVariant> params;
            params << QVariant( currentUsername() );
            params << QVariant( challenge );
            params << QVariant( auth );
            params << QVariant( artist );
            params << QVariant( tags );
            params << QVariant( modeParam );
            xmlData = formatXmlRpc( "tagArtist", params );
        }
        break;

        case ItemAlbum:
        {
            m_actionAlbum = token;

            QList<QVariant> params;
            params << QVariant( currentUsername() );
            params << QVariant( challenge );
            params << QVariant( auth );
            params << QVariant( artist );
            params << QVariant( token );
            params << QVariant( tags );
            params << QVariant( modeParam );
            xmlData = formatXmlRpc( "tagAlbum", params );
        }
        break;

        case ItemTrack:
        {
            m_actionTrack = token;

            QList<QVariant> params;
            params << QVariant( currentUsername() );
            params << QVariant( challenge );
            params << QVariant( auth );
            params << QVariant( artist );
            params << QVariant( token );
            params << QVariant( tags );
            params << QVariant( modeParam );
            xmlData = formatXmlRpc( "tagTrack", params );
        }
        break;
    }

    Http *http = new Http( m_baseHost, 80, this );
    connect( http, SIGNAL( requestFinished( int, bool ) ), this, SLOT( setTagFinished( int, bool ) ) );

    QHttpRequestHeader header( "POST", "/1.0/rw/xmlrpc.php" );
    header.setValue( "Host", m_baseHost );
    header.setContentType( "text/xml" );
    stackAppend( http, http->request( header, xmlData ) );

    LOGL( 3, m_baseHost << "\n" << xmlData.data() );
}


void
WebService::setTagFinished( int id, bool error )
{
    Http *http = stackGet( id );
    if ( http == NULL ) { return; }

    bool success = true;
    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Tag request failed", http );

        success = false;
    }
    else
    {
        QList<QVariant> retVals;
        QString error;
        bool parsed = parseXmlRpc( http->readAll(), retVals, error );

        if ( !parsed )
        {
            LOGL( 1, error );
            success = false;
        }
        else
        {
            QString response = retVals.at( 0 ).toString();
            if ( response == "OK" )
            {
                success = true;
            }
            else
            {
                success = false;
                LOGL( 1, "Tag request failed, returned: " << response );
            }
        }
    }

    stackRemove( id );
    emit tagDone( m_actionArtist, m_actionAlbum, m_actionTrack, !success );
}


void
WebService::verifyUser(
    const QString& user,
    const QString& pass,
    const QString& passLower )
{
    // Get Unix time
    time_t now; 
    time(&now);
    QString time = QString::number( now );

    // Concatenate pw hash with time
    QString auth = pass + time;
    QString authLower = passLower + time;
    // Hash the concatenated string to create auth code
    QString authMd5 = MD5Digest( auth.toUtf8() );
    QString authMd5Lower = MD5Digest( authLower.toUtf8() );

    // This happens before the handshake so we have no choice but to
    // hardcode the host
    QString host = qApp->arguments().contains( "--debug" ) ?
                   "wsdev.audioscrobbler.com" :
                   "ws.audioscrobbler.com";    

    Http* http = new Http( host, 80, this );
    QString request = "/ass/pwcheck.php?"
                      "time=" + QString( QUrl::toPercentEncoding( time ) ) +
                      "&username=" + QString( QUrl::toPercentEncoding( user ) ) +
                      "&auth=" + authMd5 +
                      "&auth2=" + authMd5Lower +
                      "&defaultplayer="
                      #ifdef WIN32
                      + QString( QUrl::toPercentEncoding( CWinUtils::FindDefaultPlayer() ) );
                      #else
                      ;
                      #endif

    connect( http, SIGNAL( requestFinished( int, bool ) ),
             this, SLOT( verifyFinished( int, bool ) ) );

    int id = http->get( request );
    stackAppend( http, id );

    LOG( 3, "Verifying user. Sending request: " << request << ", id: " << id << "\n" );

}


void
WebService::verifyFinished( int id, bool error )
{
    LOG( 4, "Verify finished, id: " << id << ", error: " << error << "\n" );

    Http* http = stackGet( id );
    if ( http == 0 ) { return; }

    if ( !http || error || http->bytesAvailable() <= 0 )
    {
        logHttpError( "Verify user request failed", http );

        emit verifyResult( AUTH_ERROR, BOOTSTRAP_DENIED );
        stackRemove( id );
        return;
    }

    QString response( http->readAll() );
    response = response.trimmed();

    LOG( 4, "Verify response: " << response << "\n" );

    BootstrapCode bootstrap;
    if ( response.contains( "BOOTSTRAP" ) )
        bootstrap = BOOTSTRAP_ALLOWED;
    else
        bootstrap = BOOTSTRAP_DENIED;

    if ( response.contains( "OK2" ) ) {
        emit verifyResult( AUTH_OK_LOWER, bootstrap );
    }
    else if ( response.contains( "OK" ) ) {
        emit verifyResult( AUTH_OK, bootstrap );
    }
    else if ( response.contains( "INVALIDUSER" ) ) {
        emit verifyResult( AUTH_BADUSER, bootstrap );
    }
    else if ( response.contains( "BADPASSWORD" ) ) {
        emit verifyResult( AUTH_BADPASS, bootstrap );
    }
    else {
        emit verifyResult( AUTH_ERROR, bootstrap );
    }

    LOG( 4, "Calling stackRemove id: " << id << "\n" );
    stackRemove( id );
}


QString
WebService::parameter( QString keyName, QString data )
{
    QStringList list = data.split( "\n" );

    for ( int i = 0; i < list.size(); i++ )
    {
        QStringList values = list[ i ].split( "=" );
        if ( values[0] == keyName )
        {
            values.removeAt( 0 );
            return QString().fromUtf8( values.join( "=" ).toAscii() );
        }
    }

    return QString( "" );
}


QStringList
WebService::parameterArray( QString keyName, QString data )
{
    QStringList result;
    QStringList list = data.split( "\n" );

    for ( int i = 0; i < list.size(); i++ )
    {
        QStringList values = list[ i ].split( "=" );
        if ( values[0].startsWith( keyName ) )
        {
            values.removeAt( 0 );
            result.append( QString().fromUtf8( values.join( "=" ).toAscii() ) );
        }
    }

    return result;
}


QStringList
WebService::parameterKeys( QString keyName, QString data )
{
    QStringList result;
    QStringList list = data.split( "\n" );

    for ( int i = 0; i < list.size(); i++ )
    {
        QStringList values = list[ i ].split( "=" );
        if ( values[0].startsWith( keyName ) )
        {
            values = values[0].split( "[" );
            values = values[1].split( "]" );
            result.append( values[0] );
        }
    }

    return result;
}


void
WebService::stackAppend( Http *http, int id )
{
    if ( m_httpStack.size() == 0 )
        emit actionStarted();

    if ( id == -1 )
        id = http->currentId();

    m_httpStack.insert( id, http );
}


Http*
WebService::stackGet( int id )
{
    return m_httpStack.value( id );
}


void
WebService::stackRemove( int id, bool keepAnimation )
{
    Http *http;
    http = m_httpStack.take( id );

    if ( http )
    {
        http->close();
//         http->deleteLater();
    }

    if ( m_httpStack.size() == 0 && !keepAnimation )
        emit actionFinished();

}

QByteArray
WebService::formatXmlRpc( QString method, QList<QVariant> params )
{
    QString xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
                      "<methodCall>"
                          "<methodName>" + method + "</methodName>" +
                          "<params>";

    foreach ( QVariant param, params )
    {
        xml += "<param><value>";

        switch ( param.type() )
        {
            case QVariant::String:
            {
                xml += "<string>" + xmlEncode( param.toString() ) + "</string>";
            }
            break;

            case QVariant::StringList:
            {
                xml += "<array><data>";

                QStringList strings = param.toStringList();
                for ( int i = 0; i < strings.count(); i++ )
                {
                    xml += "<value><string>" +
                           xmlEncode( strings.at( i ) ) +
                           "</string></value>";
                }

                xml += "</data></array>";
            }
            break;

            default:
            {
                // Support for this type not yet implemented
                Q_ASSERT( false );
            }
        } // end switch

        xml += "</value></param>";

    } // end foreach

    xml += "</params></methodCall>";

    return xml.toUtf8();
}

bool
WebService::parseXmlRpc( QByteArray xmlResponse, QList<QVariant>& returnValues, QString& error )
{
    QDomDocument xml;
    if ( !xml.setContent( xmlResponse ) )
    {
        error = "Couldn't parse XML response: " + xmlResponse;
        return false;
    }

    QDomNodeList fault = xml.elementsByTagName( "fault" );
    if ( !fault.isEmpty() )
    {
        // TODO: Parse XML RPC fault struct here
        error = "Fault present in XML response: " + xmlResponse;
        return false;
    }

    QDomNodeList params = xml.elementsByTagName( "param" );
    if ( params.isEmpty() )
    {
        error = "No params present in XML response: " + xmlResponse;
        return false;
    }

    for ( int i = 0; i < params.count(); ++i )
    {
        QDomNode node = params.at( i );

        // Skip past the pointless "<value>" tag
        QDomElement param = node.firstChildElement().firstChildElement();
        if ( param.isNull() )
        {
            error = "Malformed XML: " + xmlResponse;
            return false;
        }
        else
        {
            QVariant val = parseXmlRpcValue( param );
            returnValues << val;
        }
    }

    return true;
}


QVariant
WebService::parseXmlRpcValue( QDomElement elem )
{
    QString tag = elem.tagName();

    if ( tag == "string" )
    {
        return xmlDecode( elem.text() );
    }

    else if ( tag == "i4" || tag == "int" )
    {
        return elem.text().toInt();
    }

    else if ( tag == "struct" )
    {
        QMap<QString, QVariant> structMap;
        QDomNodeList members = elem.elementsByTagName( "member" );

        for ( int j = 0; j < members.count(); ++j )
        {
            QDomNode member = members.at( j );
            QDomElement name = member.firstChildElement( "name" );
            QDomElement value = member.firstChildElement( "value" );

            QVariant val = parseXmlRpcValue( value.firstChildElement() );

            structMap.insert( name.text(), val );
        }

        return structMap;
    }

    else if ( tag == "array" )
    {
        QList<QVariant> array;

        // Step over the <data> element down to the <value>s
        QDomNodeList kids = elem.firstChild().childNodes();

        for ( int j = 0; j < kids.count(); ++j )
        {
            QDomNode value = kids.at( j );
            if ( value.isElement() && value.toElement().tagName() == "value" )
            {
                QVariant val = parseXmlRpcValue( value.firstChildElement() );
                array << val;
            }
        }

        return array;
    }

    else
    {
        // Support for this type not yet implemented
        Q_ASSERT( false );
        return QVariant();
    }
}


void
WebService::makeXmlRpcCall( QByteArray xmlData, const char* returnSlot, bool useCache, Http* http )
{
    // REFACTOR: this is hacky, the returnSlot argument is only used if we're
    // not passing in a http object...

    if ( http == NULL )
    {
        http = new Http( this );
    }
    else
    {
        // Ugly!
        disconnect( http, SIGNAL( dataAvailable( QByteArray ) ), this, returnSlot );
    }
    
    connect( http, SIGNAL( dataAvailable( QByteArray ) ), this, returnSlot );
    http->setHost( m_baseHost );

    QHttpRequestHeader header( "POST", "/1.0/rw/xmlrpc.php" );
    header.setValue( "Host", m_baseHost );
    header.setContentType( "text/xml" );
    int id = http->request( header, xmlData, 0, useCache );
    
    // I think this should not be used here as we don't get an ID back in the return slot
    //stackAppend( http, id );

    LOGL( 3, m_baseHost << "\n" << xmlData.data() );
}


QString
WebService::xmlEncode( QString xml )
{
    // Need to escape only &, <, > in XML RPC calls
    xml.replace( "&", "&amp;" );
    xml.replace( "<", "&lt;" );
    xml.replace( ">", "&gt;" );

    return xml;
}


QString
WebService::xmlDecode( QString xml )
{
    // Need to escape only &, <, > in XML RPC calls
    xml.replace( "&amp;", "&" );
    xml.replace( "&lt;", "<" );
    xml.replace( "&gt;", ">" );

    return xml;
}


QString
WebService::challengeString()
{
    uint unixTime = QDateTime::currentDateTime().toTime_t();
    return QString::number( unixTime );
}


void
WebService::logHttpError( QString msg, Http* http )
{
    if ( http != NULL )
    {
        LOG( 1, msg << ". QHttp error code: " << http->error() << "\n" <<
            "  QHttp error text: " << http->errorString() << "\n" <<
            "  Request: " << http->currentRequest().path() << "\n" <<
            "  Bytes returned: " << http->bytesAvailable() << "\n" );
    }
    else
    {
        LOGL( 1, msg );
    }
}


Q_EXPORT_PLUGIN2( service_web, WebService )
